package org.nebuloop.photo.organizer.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Provides utility methods for common File operations.
 * 
 * <b><i>Warning: This class is not thread safe</i></b>
 * 
 */
public class FileUtil {

  private static final Logger LOGGER = Logger.getLogger(FileUtil.class.getCanonicalName());
  final MessageDigest digest;

  public FileUtil() {
    try {
      MessageDigest digest1 = java.security.MessageDigest.getInstance("MD5");
      MessageDigest finalDigest;
      synchronized (FileUtil.class) {
        try {
          // try to create a clone
          // cloned object will perform better under multi-threaded environment
          finalDigest = (MessageDigest) digest1.clone();
        } catch (CloneNotSupportedException ce) {
          LOGGER.warning("Message Digest does not support cloning. "
              + "Performance under multi-threaded execution will be affected" + ce);
          finalDigest = digest1;
        }
      }
      digest = finalDigest;
    } catch (NoSuchAlgorithmException ex) {
      throw new RuntimeException("Unable to get MD5 digest", ex);
    }
  }

  // why unnecessarily allocate and deallocate every-time
  // buffer for computing md5sum of the given file
  private final byte[] buffer = new byte[64 * 1024];

  /**
   * Computes MD5 of the given file. This method is though thread-safe, it is
   * synchronized
   * 
   * @param file
   * @return
   * @throws IOException
   */
  public String md5sum(File file) throws IOException {
    try (InputStream is = new FileInputStream(file)) {
      String strMd5sum;
      int read;
      synchronized (digest) {
        digest.reset();
        while ((read = is.read(buffer)) > 0) {
          digest.update(buffer, 0, read);
        }
        byte[] md5sum = digest.digest();
        BigInteger bigInt = new BigInteger(1, md5sum);
        strMd5sum = bigInt.toString(16);
      }
      return strMd5sum;
    } catch (IOException ex) {
      LOGGER.log(Level.INFO, "Unable to compute MD5 for file : " + file.getAbsolutePath(), ex);
      throw ex;
    }
  }

  /**
   * Copies a single file. It uses NIO.2 for copying the file with very high
   * performance
   * 
   * This method is thread-safe
   * 
   * @param sourceFile
   * @param destFile
   * @throws IOException
   */
  public static void copyFile(File sourceFile, File destFile) throws IOException {
    FileChannel source = null;
    FileChannel destination = null;
    try {
      source = new FileInputStream(sourceFile).getChannel();
      destination = new FileOutputStream(destFile).getChannel();
      destination.transferFrom(source, 0, source.size());
    } finally {
      if (source != null) {
        source.close();
      }
      if (destination != null) {
        destination.close();
      }
    }
  }

  /**
   * Create the specified directory. This method is thread-safe
   * 
   * @param destDir
   * @return
   * @throws IOException
   */
  public static boolean mkdirs(File destDir) throws IOException {
    String strDestDir;
    try {
      strDestDir = destDir.getCanonicalPath().intern();
    } catch (IOException ex) {
      LOGGER.log(Level.WARNING, "Error in file system operation for : " + destDir.toString(), ex);
      throw ex;
    }

    boolean result;
    synchronized (strDestDir) {
      result = destDir.mkdirs();
    }

    return result;
  }
}
